
系列:30 天打造企業級 PLM|面向:全端|素材:Config 模組、視覺化表單設計器
商用 PLM 最強的不是功能多,是管理員自己就能加欄位。品保明天要在表單上多一欄「檢驗標準」,不用等 IT 排程、不用改版部署,設定完存檔就生效。自建 PLM 若做不到這件事,那只是做了一套要一直改程式的表單系統。今天講 Mini-PLM 的 metadata-driven 架構:欄位定義存在資料庫裡,前後端都照定義行事。
動態欄位的儲存有三種經典路線,說穿了是資料庫正規化與反正規化的取捨:
| 路線 | 作法 | 優點 | 代價 |
|---|---|---|---|
| EAV(Entity-Attribute-Value) | 一張值表存所有欄位值,一列一值 | 完全正規化、加欄位零成本、無數量上限 | 查詢要大量 JOIN/pivot、型別全變字串、千筆表單查詢效能雪崩 |
| JSON 欄位 | 實體上一個 JSON column 裝所有動態值 | 讀寫單純、結構自由 | 跨庫支援度不一(Oracle/PG 語法與索引差異大)、動態 Criteria 查詢受限 |
| 寬表(Wide Table) | 預先挖好型別欄位池(text01..50, date01..30 等) | 原生 SQL 查詢最快、一對一關聯直讀、跨庫完全相容、索引好下 | 實體欄位有物理上限、需要定義層做槽位映射 |

在資料儲存層,Mini-PLM 與 Agile 做出了相同的務實選擇:採用「寬表欄位池」(mp_form_data 與 mp_item_data)。
以 mp_form_data 為例,資料表預先挖好了各型態的實體欄位池:
text01..text50
textarea01..textarea30
date01..date30
select01..select30 / multilist01..multilist30
radio01..radio30 / checkbox01..checkbox30
number01..number30
image01..image05 / upload01..upload05
html01..html10
OTHER_INPUT_VALUES
為什麼不選純 EAV? 如果一張表單有 30 個自訂欄位,純 EAV 查 1,000 筆表單需要掃描 30,000 列並做 30 次 self-JOIN 或記憶體 pivot,資料庫效能直接崩潰。而寬表只需要一次一對一的主鍵 JOIN,效能與原生實體表完全相同。
雖然在「資料儲存層」兩者都選擇了寬表,但在**「欄位定義機制(Metadata Definition)」**與前後端協同上,Mini-PLM 與舊時代 Agile 有著本質上的演進:
| 比較維度 | 舊時代 Oracle Agile (Java Client / Flex Attributes) | 現代 Mini-PLM (Metadata-Driven Engine) |
|---|---|---|
| 定義架構 | 深度綁死在專有 Java Client Admin 階層與私有 Data Dictionary(Admin Nodes / Page Two / Page Three) | 乾淨正規化的 JPA 定義表(mp_config_form_field / mp_config_item_field),定義本身就是標準資料 |
| 前後端連動 | JSP 伺服器端標籤庫與 Java Client (Swing) 雙軌各自解析,前後端校驗邏輯易脫鉤 | 單一事實來源(Single Source of Truth):一份定義 JSON 同時驅動後端驗證(Start Gate)與前端 ProForm 宣告式渲染 |
| 管理維運體驗 | 必須開啟笨重的 Java Client 視窗,一層層點開屬性樹逐一填寫,無法即時預覽版面 | 雙操作模式:既有快速維護表格 + Canva 式所見即所得(WYSIWYG)拖拉表單設計器,排版所見即所得 |
| 流程關卡連動 | 欄位在不同關卡的顯隱與唯讀需仰賴複雜的 Privilege / Criteria 甚至寫 Java PX | 原生內建 step 屬性:在定義中直接宣告該欄位在哪個簽核關卡顯示/必填/編輯,零程式碼達成關卡級動態欄位 |
| 排版與分組 | 版面固定由系統排版(雙欄或固定表格),無法自由分區 | 原生內建 groups(分區卡片)與 col_per_row(響應式欄數),靈活配置前端版面 |
| 動態子表格 | Table 欄位配置死板,擴充二維結構極其複雜 | 雙層定義架構:獨立表頭(mp_config_table_header)與欄位(mp_config_table_column),在寬表上優雅支撐二維動態子表 |

mp_config_form_field(節錄自資料庫綱要)就是一個欄位的完整說明書:
mp_config_form_field
field_type varchar(45) ← text/textarea/digit/date/select/multilist/radio/checkbox/image/upload/html/table
data_index varchar(45) ← 欄位 key(後端存值、前端取值都認它)
field_name varchar(200) ← 顯示名稱
required boolean ← 必填(後端 Start Gate 與前端 rules 共用)
visible boolean
pattern varchar(100) ← 正規表達式驗證
pattern_msg varchar(100) ← 驗證失敗訊息
default_value varchar(500)
col_per_row integer ← 版面:一列幾欄
groups varchar(45) ← 欄位分組(畫面上的區塊)
step integer ← 綁定流程關卡(哪一關才出現/可編輯)
order_by integer ← 排序
form_type_id FK → mp_config_form_type
listnode_id FK → mp_config_listnode ← select/multilist 的選項來源
config_table_header_id FK → mp_config_table_header ← table 型欄位的表頭定義
留意三個「一份定義、兩端共用」的欄位。required 和 pattern 同時驅動前端 antd rules 與後端驗證;listnode_id 同時餵前端下拉選項與後端存值合法性檢查;step 讓同一張表單在不同簽核關卡長出不同的欄位。定義只存一份,前後端各自解讀,永遠不會不同步。這就是 metadata-driven 的核心:單一事實來源。

除了畫面排版,Metadata-driven 的威力在於流程關卡連動:
step):同一個欄位在草稿階段(Step 1)必填,到了主管審核階段(Step 2)自動轉為唯讀,而品保專屬的「檢驗標準」只有在流程推進到品保關卡(Step 3)時才會動態浮現。欄位定義的維護介面有兩種模式,同一份資料、兩種操作體驗(ConfigForm.tsx):
// 下方區塊顯示模式:table(既有 ConfigFields 表格)或 visual(WYSIWYG 視覺設計器)
const [mode, setMode] = useState<"table" | "visual">("table");
視覺設計器實機畫面。左側元件盤(每種欄位型態一個積木,數字是既有數量)、中間畫布(依 groups 分區、拖拉排序)、右側屬性面板:

拖拉核心用 @dnd-kit,架構上只有一個關鍵約束,VisualDesigner.tsx 開頭的註解就是設計文件:
// 三欄組裝:左工具箱、中畫布、右屬性面板。
// 唯一的 DndContext 置於此層,同時包住 Toolbox 與 Canvas,
// 兩者的拖放事件才會互相辨識。
<DndContext
sensors={sensors}
collisionDetection={pointerFirstCollision}
// 插入槽在拖曳開始才撐高:需持續重新量測 droppable rect,
// 否則命中框停留在撐高前的位置
measuring={{ droppable: { strategy: MeasuringStrategy.Always } }}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
順帶一提,這個設計器就是 Day 1 說的 AI Coding 實績。從傳統表格模式進化到所見即所得拖拉介面,過去要排一季的功能,實際只花不到一週。能這麼快落地的前提,正是 metadata 層早就定義完整:設計器只是同一份欄位定義的另一種編輯器,後端一行都不用動。
["A","B"])。出口做 label 轉換時必須 JSON-aware 解析再以 ", " join,直接當字串輸出,前端會顯示成無分隔的連接字定義層正規化換彈性與單一事實來源,值層務實採用寬表槽位池換查詢極致效能。這套地基後面會一直回收:Day 8 動態表單渲染、Day 16 動態查詢、Day 30 的 AI for Setup,全部建立在「規則是資料」之上。
明日 Day 5:REST API 設計與統一回應格式——「單筆也包陣列」的教訓。